﻿// Copyright (c) MichaConrad. All rights reserved. See License.txt in the project root for license information.

namespace LinqToLdapExtensions.Tests.Scope
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading;

    using Microsoft.VisualStudio.TestTools.UnitTesting;

    /// <summary>
    /// The directory context factory test class.
    /// </summary>
    [TestClass]
    public class DirectoryContextFactoryTest 
    {
        /// <summary>
        /// The test setup.
        /// </summary>
        [TestInitialize]
        public void TestSetup()
        {   
        }

        /// <summary>
        /// Verifies that GetCurrentContext does throw an exception if no context is bound, even it we created one
        /// (binding must be done manually).
        /// </summary>
        [TestMethod]
        [ExpectedException(typeof(InvalidOperationException))]
        public void WebGetCurrentWithoutBindingShouldThrow()
        {
            TestHelper.MockHttpContext();

            var factory = TestHelper.MockFactory(ScopeState.WebContext);

            var ctxCreated = factory.CreateContext();

            var actual = factory.GetCurrentContext(); // must throw an exception because there is no bound context

            Assert.AreEqual(ctxCreated, actual);
        }
        
        /// <summary>
        /// Verifies that GetCurrentContext does throw an exception if no context is bound, even it we created one
        /// (binding must be done manually).
        /// </summary>
        [TestMethod]
        [ExpectedException(typeof(InvalidOperationException))]
        public void ThreadStaticGetCurrentWithoutBindingShouldThrow()
        {
            var factory = TestHelper.MockFactory(ScopeState.ThreadStatic);

            var ctxCreated = factory.CreateContext();

            var actual = factory.GetCurrentContext(); // must throw an exception because there is no bound context

            Assert.AreEqual(ctxCreated, actual);
        }

        /// <summary>
        /// Verifies that create context binds correctly so that GetCurrentContext returns the same element.
        /// </summary>
        [TestMethod]
        public void ThreadStaticGetCurrentContextIsEqualToCreatedOne()
        {
            var factory = TestHelper.MockFactory(ScopeState.ThreadStatic);

            var ctxCreated = factory.CreateContext();
            factory.CurrentDirectoryContextBinding.Bind(ctxCreated);

            var actual = factory.GetCurrentContext();

            Assert.AreEqual(ctxCreated, actual);
        }

        /// <summary>
        /// Verifies that context created outside of the thread calling GetCurrentContext is not bound and throws an exception.
        /// </summary>
        [TestMethod]
        public void ThreadStaticGetCurrentContextIsNotBoundInDifferentThread()
        {
            var factory = TestHelper.MockFactory(ScopeState.ThreadStatic);

            // opening context outside the thread
            var ctxCreated = factory.CreateContext();
            Assert.IsNotNull(ctxCreated);

            factory.CurrentDirectoryContextBinding.Bind(ctxCreated);

            var hasThrown = false;
            var t = new Thread(() =>
            {
                try
                {
                    // thread should not have a bound context and therefore should throw an exception.
                    factory.GetCurrentContext();
                }
                catch (InvalidOperationException)
                {
                    hasThrown = true;
                }
            });
            t.Start();
            t.Join();
            Assert.IsTrue(hasThrown, "Should have thrown an exception...");
        }

        /// <summary>
        /// Testing thread safety by running multiple threads in parallel accessing the same factory.
        /// </summary>
        [TestMethod]
        public void ThreadStaticGetCurrentContextVerifyIsThreadSafe()
        {
            var factory = TestHelper.MockFactory(ScopeState.ThreadStatic);

            // also create on context within the outer thread to make sure this will not get 
            // reused (which happens with async Task.Run()...)
            var ctxCreated = factory.CreateContext();
            Assert.IsNotNull(ctxCreated);

            factory.CurrentDirectoryContextBinding.Bind(ctxCreated);

            var errors = TestHelper.RunThreadedTest(
                threads: 20,
                iterations: 50,
                test: () =>
                    {
                        var ctx = factory.CreateContext();
                        factory.CurrentDirectoryContextBinding.Bind(ctx);

                        var current = factory.GetCurrentContext();

                        Assert.AreEqual(ctx, current);

                        factory.CurrentDirectoryContextBinding.Unbind();
                    });

            Assert.IsTrue(!errors.Any());

            factory.CurrentDirectoryContextBinding.Unbind();
        }

        /// <summary>
        /// Testing thread safety by running multiple threads in parallel accessing multiple factories.
        /// This should create multiple entries within the thread static dictionary.
        /// </summary>
        [TestMethod]
        public void ThreadStaticGetCurrentContextVerifyIsThreadSafeMultiFactories()
        {
            var factories = new List<DirectoryContextFactory>();
            for (var i = 0; i < 5; i++)
            {
                var factory = TestHelper.MockFactory(ScopeState.ThreadStatic);
                factories.Add(factory);
            }

            var errors = TestHelper.RunThreadedTest(
                threads: 20,
                iterations: 50,
                test: () =>
                {
                    for (var f = 0; f < factories.Count(); f++)
                    {
                        var factory = factories[f];
                        var ctx = factory.CreateContext();

                        factory.CurrentDirectoryContextBinding.Bind(ctx);
                        ////Thread.Sleep(100); // reverse wait

                        var current = factory.GetCurrentContext();

                        Assert.AreEqual(ctx, current);

                        factory.CurrentDirectoryContextBinding.Unbind();
                    }
                });
            
            Assert.IsTrue(!errors.Any());
        }
    }
}
